package rad.framework.testing;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import javax.faces.model.SelectItem;
import javax.persistence.EntityManager;

import rad.framework.embedded.ContainerTestCase;
import rad.framework.jpa.TransactionScript;
import rad.framework.jsf.RadFrameworkFunctions;
import rad.framework.lang.DateUtil;
import rad.framework.lang.ReflectionUtil;
import rad.framework.richfaces.BaseTreeNode;
import rad.framework.richfaces.EntityTreeNode;
import rad.framework.richfaces.FolderTreeNode;
import rad.framework.richfaces.Picker;
import rad.framework.richfaces.RootTreeNode;
import rad.framework.seam.AbstractEditor;
import rad.framework.seam.AbstractFinder;
import rad.framework.seam.ActionMetadata;
import rad.framework.seam.ColumnType;
import rad.framework.seam.Filter;
import rad.framework.seam.Filter.Parameter;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SeamCrudTest<E> extends ContainerTestCase {
	
	protected static final Log log = LogFactory.getLog(SeamCrudTest.class);
	
	protected Integer pageSize = 20;

	protected String persistenceUnitJndiName = "";

	protected String conversationId;

	protected int initialRowCount = -1;

	protected Object id;
	
	//---------------------------------------------------------------

	public SeamCrudTest(String name, String persistenceUnitJndiName, int pageSize) {
		super(name);
		this.persistenceUnitJndiName = persistenceUnitJndiName;
		this.pageSize = pageSize;
	}

	protected void setUp() throws Exception {
		super.setUp();
		login();
	}

	/**
	 * Override this as necessary and use IdentityUril.addUser() to setup test user data
	 */
	protected void login() throws Exception {
		login("user", "user");
	}

	public void testCrud() throws Exception {
		initData();
		search(0);
		create();
		persist();
		update();
		search(1);
		select();
		delete();
		search(0);
	}
	
	//---------------------------------------------------------------
	// Metadata
	//---------------------------------------------------------------
	
	protected List<Value> values = new ArrayList<Value>();

	protected List<String> lists = new ArrayList<String>();

	protected List<String> charts = new ArrayList<String>();

	protected static class Value {
		public String name;
		public String expression;
		public Object value;
		public boolean hasDefault;

		public Value(String name, String expression, Object value, boolean hasDefault) {
			this.name = name;
			this.expression = expression;
			this.value = value;
			this.hasDefault = hasDefault;
		}
	}

	protected void addValue(String name, Object value, boolean hasDefault) {
		values.add(new Value(name, "#{" + getInstanceName() + "." + name + "}",
				value, hasDefault));
	}

	protected void addList(String name) {
		lists.add("#{" + name + "}");
	}

	protected void addChart(String name) {
		charts.add("#{" + getFinderName() + "." + name + "}");
	}
	
	//---------------------------------------------------------------

	protected void initData() throws Exception {
		new TransactionScript(persistenceUnitJndiName) {
			protected Object run(EntityManager em) {
				initData(em);
				return null;
			}
		}.run();
	}

	protected void initData(EntityManager em) {
	}
	
	//---------------------------------------------------------------

	/**
	 * Simulate opening the ui for the first time
	 */
	protected void search(final int count) throws Exception {
		new FinderFacesRequest(getSearchName()) {
			protected void updateModelValues() throws Exception {
				setValue("#{" + getFinderName() + ".pager.pageSize}", pageSize);
			}

			protected void invokeApplication() {
				invokeMethod("#{pager.findFirstPage}");
			}

			protected void renderResponse() throws Exception {
				List<?> list = getResult();
				
				if (initialRowCount == -1) {
					initialRowCount = list.size();
				}
				assertEquals("row count ", count + initialRowCount, list.size());

				assertTitle();
				assertColumns();
				assertIsRendered();
				assertFilters();
				assertCharts();
			}
		}.run();
	}

	//---------------------------------------------------------------
	
	/**
	 * Simulate clicking the Create button and navigating to a blank form
	 */
	protected void create() throws Exception {
		conversationId = new EditorFacesRequest(getEditName()) {
			protected void renderResponse() {
				assertNotManaged();
				assertNewInstance();
				assertConversation();
				// a conversation should have begun based on pages.xml
			}
		}.run();
	}
	
	/**
	 * Simulate clicking the Save button to insert a new object
	 */
	protected void persist() throws Exception {
		new EditorFacesRequest(getEditName(), conversationId) {
			protected void processValidations() throws Exception {
				validateValues();
			}

			protected void updateModelValues() throws Exception {
				setValues();
			}

			protected void invokeApplication() {
				invokeMethod("#{" + getEditorName() + ".save}");
			}

			protected void renderResponse() {
				id = getId(); // store for later
				printMessages();

				assertManaged();
				assertId();
                assertValues();
                assertLists();
                assertTree();
                assertStatusActions();
                assertConversation();
				// the conversation should still exist
			}
		}.run();
	}

	/**
	 * Simulate clicking the Save button to update the current object
	 */
	protected void update() throws Exception {
		new EditorFacesRequest(getEditName(), conversationId) {
			protected void processValidations() throws Exception {
				validateValues();
			}

			protected void updateModelValues() throws Exception {
				setValues();
			}

			protected void invokeApplication() {
				invokeMethod("#{" + getEditorName() + ".save}");
			}

			protected void renderResponse() {
				printMessages();
				
				assertManaged();
				assertId();
                assertValues();
				assertConversation();
				// the conversation should still exist
			}
		}.run();
	}

	/**
	 * Simulate clicking the View button on a row in a data table and navigating
	 * to the edit view
	 */
	protected void select() throws Exception {
		conversationId = new EditorFacesRequest(getViewName()) {
			protected void beforeRequest() {
				setIdParameter();
			}

			protected void renderResponse() {
				assertManaged();
				assertId();
				assertIsRendered();
				assertConversation();
				// a conversation should have begun based on pages.xml
			}
		}.run();
	}
	
	/**
	 * Simulate clicking the Delete button and returning to the search view
	 */
	protected void delete() throws Exception {
		new EditorFacesRequest(getSearchName()) {
			protected void beforeRequest() {
				setIdParameter();
			}

			protected void invokeApplication() {
				invokeMethod("#{" + getFinderName() + ".delete}");
			}
		}.run();
	}

	//---------------------------------------------------------------
	// Base Finder Request
	//---------------------------------------------------------------
	
	public class FinderFacesRequest extends FacesRequest {

		public FinderFacesRequest() {
			super();
		}

		public FinderFacesRequest(String viewId, String conversationId) {
			super(viewId, conversationId);
		}

		public FinderFacesRequest(String viewId) {
			super(viewId);
		}
		
		//---------------------------------------

		protected AbstractFinder<?> getFinder() {
			return (AbstractFinder<?>) getValue("#{" + getFinderName() + "}");
		}

		protected List<?> getResult() {
			return (List<?>) getValue("#{" + getFinderName() + ".result}");
		}

		protected String assertTitle() {
			return (String) getValue("#{" + getFinderName() + ".title}");
		}
		
		protected void assertIsRendered() {
			AbstractFinder<?> finder = getFinder();
			finder.isRenderAction();
			finder.isSimpleEntity();
			finder.isRenderCharts();
			finder.isRenderCreate();
			finder.isRenderDelete();
			finder.isRenderEdit();
			finder.isRenderExcel();
			finder.isRenderExport();
			finder.isRenderImport();
			finder.isRenderPrint();
			finder.isRenderPrintDetails();
			finder.isRenderQuickAdd();
			finder.isRenderView();
		}

		protected void assertCharts() {
			for (String name : charts) {
				try {
					getValue(name);
				} catch (Exception e) {
					throw new RuntimeException("Error accessing chart: "
							+ name, e);
				}
			}
		}
		
		protected void assertColumns() {
			List<ColumnType> columns = (List<ColumnType>) getValue("#{" + getFinderName() + ".columns}");
			for (ColumnType column : columns) {
				exerciseColumn(column);
			}
		}

		protected void assertFilters() {
			Filter filter = (Filter) getValue("#{" + getFinderName() + ".filter}");
			assertNotNull(filter);
			assertParamaters(filter.getStrings());
			assertParamaters(filter.getBooleans());
			assertParamaters(filter.getEnums());
			assertParamaters(filter.getDates());
			assertParamaters(filter.getNumbers());
			assertParamaters(filter.getM2os());
			assertParamaters(filter.getO2ms());
			assertParamaters(filter.getM2ms());
		}
		
		protected void assertParamaters(List<?> params) {
			List<Parameter> parameters = (List<Parameter>) params;
			for (Parameter parameter : parameters) {
				parameter.getLabel();
				log.debug("Parameter Name: " + parameter.getName());
				log.debug("Parameter Label: " + parameter.getLabel());
			}
		}
	}

	//---------------------------------------------------------------
	// Base Editor Request
	//---------------------------------------------------------------
	
	public class EditorFacesRequest extends FacesRequest {

		public EditorFacesRequest() {
			super();
		}

		public EditorFacesRequest(String viewId, String conversationId) {
			super(viewId, conversationId);
		}

		public EditorFacesRequest(String viewId) {
			super(viewId);
		}
		
		//---------------------------------------
		
		protected AbstractEditor<?> getEditor() {
            return (AbstractEditor<?>) getValue("#{" + getEditorName() + "}");
		}
		
		protected Object getInstance() {
            return getEditor().getInstance();
		}

		protected Object getId() {
			return getValue("#{" + getInstanceName() + ".id}");
		}
		
		protected void setIdParameter() {
			// simulate selection
			log.debug("ID Parameter: " + id.toString());
			setParameter("id", id.toString());
		}
		
		protected void setConversationPropagation(String value) {
			setParameter("conversationPropagation", value);
		}
		
		protected void validateValues() {
			for (Value value : values) {
				validateValue(value.expression, value.value);
			}
			assertValid();
		}
		
		protected void setValues() throws Exception {
			for (Value value : values) {
				setValue(value.expression, value.value);
			}
		}
		
		protected void assertId() {
			assertNotNull("id not null", getId());
		}

		protected void assertIsRendered() {
			AbstractEditor<?> editor = getEditor();
			editor.isTop();
			editor.isCurrentNode();
			editor.isEditMode();
			editor.isViewMode();
			editor.isEndConversation();
			editor.isRenderEdit();
			editor.isRenderView();
			editor.isRenderCreate();
			editor.isRenderCreateClone();
			editor.isRenderDelete();
			editor.isRenderExport();
			editor.isRenderPrint();
			editor.isRenderCharts();
		}
//      log.debug("Editable: " + RadFrameworkFunctions.editable(instance));
		
		protected void assertNewInstance() {
			for (Value value : values) {
				if (value.hasDefault) {
					assertNotNull("assertNotNull: " + value.expression,
							getValue(value.expression));
				} else {
					assertNull("assertNull: " + value.expression,
							getValue(value.expression));
				}
                excerciseRules(getInstance(), value.name);
			}
		}
		
		protected void assertValues() {
			for (Value value : values) {
				assertEquals(value.expression + " ", value.value,
						getValue(value.expression));
                excerciseRules(getInstance(), value.name);
			}
		}
		
		protected void assertManaged() {
			assertTrue("should be managed", (Boolean) getValue("#{" + getEditorName()
					+ ".managed}"));
		}

		protected void assertNotManaged() {
			assertFalse("should not be managed", (Boolean) getValue("#{"
					+ getEditorName() + ".managed}"));
		}
		
		protected void assertConversation() {
			assertTrue("should be in conversation", isLongRunningConversation());
		}
		
		protected void assertNotConversation() {
			assertFalse("should not be in conversation", isLongRunningConversation());
		}

		protected void assertLists() {
			// test combobox and picker lists
			for (String name : lists) {
				try {
					String variable = name.substring(0, name.indexOf(".")) + "}";
					assertNotNull(name + " not found", getValue(variable));
					
					List<SelectItem> items = (List<SelectItem>) getValue(name);
					assertNotNull(name + " List not found", items);
					for (SelectItem selectItem : items) {
	                	log.debug(name + ":" + selectItem.getValue() + ":" + selectItem.getLabel());
					}
				} catch (Exception e) {
					throw new RuntimeException("Error accessing list: "
							+ name, e);
				}
			}
		}

		protected void assertTree() {
            RootTreeNode tree = (RootTreeNode) getValue("#{tree}");
            exerciseTree(tree);
		}

		protected void assertStatusActions() {
            List<ActionMetadata> machine = getEditor().getStatusActions();
            for (ActionMetadata actionMetadata : machine) {
            	log.debug("Action: " + actionMetadata.getValue());
            	log.debug("Action Label: " + actionMetadata.getLabel());
            	log.debug("Action Title: " + actionMetadata.getTitle());
            	log.debug("Action Rendered: " + actionMetadata.isRendered());
            	log.debug("Action Disabled: " + actionMetadata.isDisabled());
			}
		}
	}

	protected void exerciseColumn(ColumnType column) {
		log.debug("Column Name: " + column.getName());
		log.debug("Column Type: " + column.getType());
		log.debug("Column Label: " + column.getLabel());
		log.debug("Column Value: " + column.getValue()); // TODO only in data loop
	}

	protected void exerciseTree(BaseTreeNode node) {
		log.debug("Node Text: " + node.getText());
		log.debug("Node Key: " + node.getKey());
		log.debug("Node Master Node: " + node.isMasterNode());
		log.debug("Node Current Node: " + node.isCurrentNode());
		log.debug("Node Leaf Node: " + node.isLeaf());
		
		if (node instanceof EntityTreeNode) {
			EntityTreeNode<?> entity = (EntityTreeNode<?>) node;
			log.debug("Entity Node Type Text: " + entity.getEntityTypeText());
			log.debug("Entity Node Editor: " + entity.getEditor());
		}
		
		if (node instanceof FolderTreeNode) {
			FolderTreeNode<?> folder = (FolderTreeNode<?>) node;
			exerciseFolder(folder);
			
			List<ColumnType> columns = folder.getColumns();
			for (ColumnType column : columns) {
				exerciseColumn(column);
			}
			
			Object picker = folder.getPicker();
			if (picker != null && picker instanceof Picker) {
				Picker p = (Picker) picker;
				log.debug("Picker: " + p);
				log.debug("Picker Rendered: " + p.isRendered());
				
				//p.setFilter("x"); // TODO folder returns null
//				List<SelectItem> list = p.getAvailable();
//        		for (SelectItem item : list) {
//        			log.debug("Picker Available: " + item.getValue() + ", " + item.getLabel());
//				}
//				list = p.getSelected();
//        		for (SelectItem item : list) {
//        			log.debug("Picker Selected: " + item.getValue() + ", " + item.getLabel());
//				}
        		// TODO add/remove
			}
		}
		
        Iterator i = node.getChildren();
        while (i.hasNext()) {
        	Entry entry = (Entry) i.next();
            exerciseTree((BaseTreeNode) entry.getValue());
		}
	}
	
	protected void exerciseFolder(FolderTreeNode<?> folder) {
		log.debug("Folder Node Entity Type Text: " + folder.getEntityTypeText());
//		log.debug("Folder Node Editor: " + folder.getEditor()); // TODO need to reset editor instance?
		
		log.debug("Folder Node Many 2 Many: " + folder.isManyToMany());
		log.debug("Folder Node One 2 Many: " + folder.isOneToMany());
		log.debug("Folder Node One 2 Many Shared: " + folder.isOneToManyShared());
		log.debug("Folder Node One 2 One: " + folder.isOneToOne());

		log.debug("Folder Node Multipart: " + folder.isMultipart());
		log.debug("Folder Node Opened: " + folder.isOpened());
		log.debug("Folder Node Orderable: " + folder.isOrderable());
		log.debug("Folder Node Render Action: " + folder.isRenderAction());
		
		if (folder.isManyToMany()) {
			log.debug("Folder Node Render Picker: " + folder.isRenderPicker());
		} else if (folder.isOneToOne()) {
			log.debug("Folder Node Render One to One View: " + folder.isRenderOneToOneView());
			log.debug("Folder Node Render One to One Edit: " + folder.isRenderOneToOneEdit());
		} else {
			boolean renderQuickAdd = folder.isRenderQuickAdd();
			boolean renderAdd = folder.isRenderAdd();
			log.debug("Folder Node Render Quick Add: " + renderQuickAdd);
			log.debug("Folder Node Render Add: " + renderAdd);
			assertFalse("renderQuickAdd and renderAdd can not both be true", renderQuickAdd && renderAdd);
		}
		log.debug("Folder Node Render Table: " + folder.isRenderTable());
	}
	
	protected void excerciseRules(Object instance, String name) {
		// TODO replace with reflection based calls: getValue(#{class.prop.rule})

		if (name.contains(".")) { // TODO handle embeddeds, reflection
			return;
		}

		log.debug("Name: " + name);
		log.debug("Required: " + RadFrameworkFunctions.required(instance, name));
		log.debug("Rendered: " + RadFrameworkFunctions.rendered(instance, name));
		log.debug("Disabled: " + RadFrameworkFunctions.disabled(instance, name));
		log.debug("ReRender: " + RadFrameworkFunctions.reRender(instance, name));

		Object value = ReflectionUtil.getProperty(instance, name);
		if (value != null && value.getClass().isEnum()) {
			List<SelectItem> list = RadFrameworkFunctions.enumConstants(instance,
					name);
			for (SelectItem item : list) {
				log.debug("Enum Item: " + item.getValue() + ", "
						+ item.getLabel());
			}
		}
//        	if (false) {
//        		ComboBox combo = null;//(ComboBox) property;
//        		List<SelectItem> list = combo.filter("");
//        		for (SelectItem item : list) {
//        			if (!combo.isFiltered() && !item.getValue().getClass().isAnnotationPresent(Entity.class)) {
//        				throw new RuntimeException(name + " does not have entity for value.");
//        			}
//        			log.debug("Combo Item: " + item.getValue() + ", " + item.getLabel());
//				}
//        	}
	}
	
	// ------------------------------------
	// helpers
	// ------------------------------------

	protected Date truncateDate(Date date) {
		return DateUtil.truncateDate(date);
	}

	protected Date truncateTime(Date date) {
		return DateUtil.truncateDate(date);
	}

	protected Date addDays(int days) {
		return truncateDate(DateUtil.addDays(days));
	}

	// ------------------------------------
	// type related
	// ------------------------------------

	private Class<?> entityClass;

	public Class<?> getEntityClass() {
		if (entityClass == null) {
			entityClass = ReflectionUtil.getParameterizedType(getClass());
		}
		return entityClass;
	}

	public String getEntityName() {
		return getEntityClass().getSimpleName();
	}

	protected String getFinderName() {
		return StringUtils.uncapitalize(getEntityName()) + "Finder";
	}

	protected String getEditorName() {
		return StringUtils.uncapitalize(getEntityName()) + "Editor";
	}

	protected String getTreeName() {
		return StringUtils.uncapitalize(getEntityName()) + "Tree";
	}

	protected String getInstanceName() {
		return StringUtils.uncapitalize(getEntityName());
	}

	protected String getViewPath() {
		return "";
	}

	protected String getEditName() {
		return "/jsf/" + getViewPath() + getEntityName() + "/Edit.xhtml";
	}

	protected String getViewName() {
		return "/jsf/" + getViewPath() + getEntityName() + "/View.xhtml";
	}

	protected String getSearchName() {
		return "/jsf/" + getViewPath() + getEntityName() + "/Search.xhtml";
	}
}
